package br.com.massai.view.utils;

import java.beans.PropertyDescriptor;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;

import org.apache.commons.beanutils.BeanUtils;
import org.apache.commons.beanutils.PropertyUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

import br.com.massai.model.exceptions.ErrorCore;
import br.com.massai.model.exceptions.SystemException;

@SuppressWarnings({ "all" })
public class JavaBeanUtil {
	
	private JavaBeanUtil() { }
	
	private static final Logger logger = LogManager.getLogger(JavaBeanUtil.class);

	// Constantes com as mensagens para os logs de Exceções
	private static final String NAO_FOI_POSSIVEL_ACESSAR_METODO = "não foi possível acessar o método.";
	private static final String ERRO_INESPERADO = "Erro inesperado.";
	private static final String EXCECAO_LANCADA_PELO_METODO_ALVO = "Exceção lançada pelo método alvo.";

	private static Set<Class> wrapperTypes;

	static {
		loadWrappers();
	}

	/**
	 * Retorna a representação de um JavaBean em um Map, onde as chaves são os
	 * nomes dos atributos do objeto e os valores serao os valores dos mesmos.
	 * Se o parametro informado for nulo, sera retornado um Map vazio.
	 * 
	 * @param bean
	 *            O objeto do qual se deseja a representação em Map.
	 * @return Um Map contendo os atributos e os valores do JavaBean. A chave do
	 *         mapa eh o atributo do bean.
	 * @throws SystemException
	 *             Caso ocorra algum problema na obtenção dos valores contidos
	 *             no JavaBean.
	 */
	public static Map beanToMap(Object bean) throws SystemException {

		Map hm = new HashMap();

		try {
			hm = describe(bean);
		} catch (Exception e) {
			tratarExcecao(e);
		} // try - catch

		return hm;

	} // beanToMap()

	/**
	 * Retorna um objeto populado com os valores informados no Mapa, onde a
	 * chave deve ser o mesmo nome do atributo no JavaBean. Se o Map informado
	 * for nulo, sera retornado um objeto nulo.
	 * 
	 * @param hmValores
	 *            Um Map com os pares (chave-valor) utilizados para popular o
	 *            JavaBean.
	 * @param classe
	 *            A classe do JavaBean que deve ser retornado.
	 * @return Object com os valores do Map.
	 * @throws SystemException
	 *             Caso ocorra alguma problema no povoamento do JavaBean.
	 */
	public static Object mapToBean(Map hmValores, Class classe)
			throws SystemException {

		Object bean = null;

		if (hmValores != null) {

			try {
				bean = novaInstancia(classe);
				BeanUtils.populate(bean, hmValores);
			} catch (Exception e) {
				tratarExcecao(e);
			} // try - catch

		} // if

		return bean;

	} // beanToMap()

	/**
	 * Popula o JavaBean com os valores do Map.
	 * 
	 * @param bean
	 *            o objeto (JavaBean) que sera populado.
	 * @param hmValores
	 *            O Map com os valores para os atributos do JavaBean
	 * @return o JavaBean com os valores do Map.
	 * @throws SystemException
	 *             Caso ocorra alguma exceção de sistema.
	 */
	public static Object populate(Object bean, Map hmValores)
			throws SystemException {

		if (bean != null && hmValores != null) {

			try {
				BeanUtils.populate(bean, hmValores);
			} catch (Exception e) {
				tratarExcecao(e);
			} // try - catch
		} // if

		return bean;

	} // populate()

	/**
	 * Retorna o clone do objet informado.
	 * 
	 * @param bean
	 *            o objeto a ser clonado.
	 * @return o clone do objet informado.
	 * @throws SystemException
	 *             Caso ocorra alguma exceção ao tentar fazer o clone do objeto.
	 */
	public static Object cloneBean(Object bean) throws SystemException {

		Object clone = null;

		if (bean != null) {

			try {
				clone = BeanUtils.cloneBean(bean);
			} catch (Exception e) {
				tratarExcecao(e);
			}

		}

		return clone;
	}

	/**
	 * Copia as propriedades do bean de origem para o bean destino, as
	 * propriedades devem ter o mesmo nome.
	 * 
	 * @param destino
	 *            O bean onde as propriedades serao atribuidas.
	 * @param origem
	 *            O bean de onde as propriedades serao extraídas.
	 * @throws SystemException
	 *             caso ocorra algum erro ao copiar o bean
	 */
	public static void copy(Object destino, Object origem)
			throws SystemException {
		try {
			BeanUtils.copyProperties(destino, origem);
		} catch (Exception e) {
			tratarExcecao(e);
		}
	}

	/**
	 * Seta a propriedade no bean.
	 * 
	 * @param destino
	 *            O bean onde as propriedades serao atribuidas.
	 * @param propriedade
	 *            O nome da propriedade a ser atribuida.
	 * @param valor
	 *            O valor da propriedade.
	 * @throws SystemException
	 *             caso ocorra algum erro ao setar a propriedade no bean
	 */
	public static void setProperty(Object destino, String propriedade,
			Object valor) throws SystemException {
		try {
			BeanUtils.setProperty(destino, propriedade, valor);
		} catch (Exception e) {
			tratarExcecao(e);
		}
	}

	/**
	 * Obtem a propriedade.
	 * 
	 * @param origem
	 *            um Object contendo o objeto cuja propriedade sera obtida
	 * @param propriedade
	 *            O nome da propriedade a ser atribuida.
	 * @return um Object contendo a propriedade
	 * @throws SystemException
	 *             caso ocorra algum erro na obtenção da propriedade
	 */
	public static Object obterProperty(Object origem, String propriedade)
			throws SystemException {

		Object o = null;

		try {
			o = PropertyUtils.getProperty(origem, propriedade);
		} catch (Exception e) {
			tratarExcecao(e);
		}

		return o;
	}

	/**
	 * Trata a exceção.
	 * 
	 * @param e
	 *            A exceção que deve ser tratada.
	 * @throws SystemException
	 *             Uma exceção encapsulada.
	 */
	private static void tratarExcecao(Exception e) throws SystemException {

		String mensagem = "";
		if (e instanceof IllegalAccessException) {
			mensagem = NAO_FOI_POSSIVEL_ACESSAR_METODO;
		} else if (e instanceof InvocationTargetException) {
			InvocationTargetException ex = (InvocationTargetException) e;
			mensagem = EXCECAO_LANCADA_PELO_METODO_ALVO
					+ ex.getTargetException();
		} else {
			mensagem = ERRO_INESPERADO;
		} // try - catch

		logger.error(mensagem, e);
		throw new SystemException(ErrorCore.ERROR_SYSTEM, e);

	} // tratarExcecao

	/**
	 * Retorna a representação de um JavaBean em um Map, onde as chaves são os
	 * nomes dos atributos do objeto e os valores serao os valores dos mesmos.
	 * Se o parametro informado for nulo, sera retornado um Map nulo. <b>OBS:
	 * não foi utilizado o describe do BeanUtils devido a ele retornar todos os
	 * valores do Mapa como String. </b>
	 * 
	 * @param bean
	 *            O objeto do qual se deseja a representação em Mapa
	 * @return Um Mapa com as chaves/valores iguais aos atributos/valores do TO
	 * @throws IllegalArgumentException
	 *             Caso haja alguma exceção no processo de reflexão.
	 * @throws IllegalAccessException
	 *             Caso haja alguma exceção no processo de reflexão.
	 * @throws InvocationTargetException
	 *             Caso haja alguma exceção no processo de reflexão.
	 */
	private static Map describe(Object bean) throws IllegalArgumentException,
			IllegalAccessException, InvocationTargetException {

		Map<String, Object> hmRetorno = null;

		if (bean != null) {

			hmRetorno = new HashMap<String, Object>();
			Method metodo;
			String noAtributo;
			Object vlAtributo;

			Method[] metodos = bean.getClass().getMethods();

			for (int i = 0; i < metodos.length; i++) {

				metodo = metodos[i];

				if (isMetodoGetValido(metodo)) {

					noAtributo = obterNomeAtributo(metodo);
					vlAtributo = metodo.invoke(bean);

					if (vlAtributo != null) {
						hmRetorno.put(noAtributo, vlAtributo);
					}

				} // if

			} // while

		} // if

		return hmRetorno;

	} // describe()

	/**
	 * Retorna se eh um método get válido.
	 * 
	 * @param metodo
	 *            um Method contendo o método a ser validado
	 * @return true se for um método get válido, false caso contrário.
	 */
	private static boolean isMetodoGetValido(Method metodo) {
		return metodo.getName().startsWith("get")
				&& !metodo.getName().equals("getClass");
	}

	/**
	 * Retorna se o retorno do método eh primitivo ou wrapper ou String.
	 * 
	 * @param metodo
	 *            um Method contendo o método a ser verificado
	 * @return <b>true</b> se o retorno do método eh primitivo ou wrapper ou
	 *         String, <b>false</b> caso contrário.
	 */
	protected static boolean isRetornoPrimitivoWrapperString(Method metodo) {

		Class<?> tipoRetorno = metodo.getReturnType();

		boolean isPrimitivo = tipoRetorno.isPrimitive();
		boolean isWrapper = wrapperTypes.contains(tipoRetorno);
		boolean isString = tipoRetorno.equals(String.class);

		return isPrimitivo || isWrapper || isString;

	} // isRetornoPrimitivoWrapperString()

	/**
	 * Retornar o nome do atributo atavésdo seu método get.
	 * 
	 * @param metodo
	 *            O método do qual se deseja o nome do atributo.
	 * @return String Uma String com o nome do atributo.
	 */
	private static String obterNomeAtributo(Method metodo) {

		String nomeAtributo = metodo.getName().substring(3);

		String primeiraLetra = nomeAtributo.substring(0, 1);
		primeiraLetra = primeiraLetra.toLowerCase();

		StringBuffer sb = new StringBuffer(primeiraLetra);
		sb.append(nomeAtributo.substring(1));

		return sb.toString();

	} // obterNomeAtributo()

	/**
	 * Carrega as classes Wrappers.
	 */
	private static void loadWrappers() {
		wrapperTypes = new HashSet<Class>();
		wrapperTypes.add(Byte.class);
		wrapperTypes.add(Short.class);
		wrapperTypes.add(Character.class);
		wrapperTypes.add(Integer.class);
		wrapperTypes.add(Long.class);
		wrapperTypes.add(Float.class);
		wrapperTypes.add(Double.class);
		wrapperTypes.add(Boolean.class);
	}

	/**
	 * Retorna uma nova instancia da classe informada.
	 * 
	 * @param classe
	 *            a classe
	 * @return uma nova instancia da classe informada.
	 * @throws SystemException
	 *             caso ocorra algum erro ao obter uma nova instancia da classe
	 *             informada
	 */
	public static Object novaInstancia(Class<?> classe) throws SystemException {

		Object instancia = null;

		try {
			instancia = classe.newInstance();
		} catch (Exception ex) {
			logger.error("Error: ",ex);
			throw new SystemException(ErrorCore.ERROR_SYSTEM, ex);
		}

		return instancia;
	}

	/**
	 * Verifica se a propriedade eh primitiva.
	 * 
	 * @param o
	 *            objeto que a propriedade sera extraída.
	 * @param propriedade
	 *            um String que representa o nome da propriedade.
	 * @return true se for primitivo, false caso não seja.
	 * @throws Exception
	 *             caso ocorra algum erro
	 */
	public static boolean isPrimitivo(Object o, String propriedade)
			throws Exception {

		Boolean retorno = null;

		try {
			PropertyDescriptor sp = PropertyUtils.getPropertyDescriptor(o,
					propriedade);
			Method metodo = PropertyUtils.getReadMethod(sp);
			Class<?> clazz = metodo.getReturnType();
			retorno = clazz.isPrimitive();
		} catch (Exception ex) {
			logger.error("Error: ",ex);
			throw new SystemException(ErrorCore.ERROR_SYSTEM, ex);
		}

		return retorno;
	}

	/**
	 * Verifica se a propriedade foi definida na classe tem annotation passada
	 * como parametro.
	 * 
	 * @param o
	 *            objeto que a propriedade sera extraída.
	 * @param propriedade
	 *            a propriedade.
	 * @param annotation
	 *            a annotation.
	 * @return true se tiver a annotation false caso não tenha.
	 * @throws SystemException
	 *             Caso ocorra alguma exceção ao executar o método.
	 */
	public static boolean isAnnotationPresenteNoMetodo(Object o,
			String propriedade, Class<? extends Annotation> annotation)
			throws SystemException {
		Boolean retorno = null;

		try {
			PropertyDescriptor sp = PropertyUtils.getPropertyDescriptor(o,
					propriedade);
			if (sp == null) {
				retorno = false;
			} else {
				Method metodo = PropertyUtils.getReadMethod(sp);
				if (metodo != null) {
					retorno = metodo.isAnnotationPresent(annotation);
				} else {
					retorno = false;
				}
			}
		} catch (Exception ex) {
			logger.error("Error: ",ex);
			throw new SystemException(ErrorCore.ERROR_SYSTEM, ex);
		}

		return retorno;
	}

	/**
	 * Obtem a anotação no método getter de um atributo.
	 * 
	 * @param o
	 *            objeto que a propriedade sera extraída.
	 * @param propriedade
	 *            a propriedade.
	 * @param annotationClass
	 *            um Class contendo a annotation.
	 * @return a annotation se ele existir.
	 * @throws SystemException
	 *             Caso ocorra alguma exceção ao obter a annotation.
	 */
	public static Annotation obterAnnotationNoMetodo(Object o,
			String propriedade, Class<? extends Annotation> annotationClass)
			throws SystemException {
		Annotation retorno;
		try {
			PropertyDescriptor sp = PropertyUtils.getPropertyDescriptor(o,
					propriedade);
			if (sp == null) {
				retorno = null;
			} else {
				Method metodo = PropertyUtils.getReadMethod(sp);
				if (metodo != null) {
					retorno = metodo.getAnnotation(annotationClass);
				} else {
					retorno = null;
				}
			}
		} catch (Exception ex) {
			logger.error("Error: ",ex);
			throw new SystemException(ErrorCore.ERROR_SYSTEM, ex);
		}

		return retorno;
	}

	/**
	 * Verifica se a propriedade foi definida na classe tem annotation passada
	 * como parametro.
	 * 
	 * @param o
	 *            objeto que a propriedade sera extraída.
	 * @param propriedade
	 *            a propriedade.
	 * @param annotation
	 *            a annotation.
	 * @return true se tiver a annotation false caso não tenha.
	 * @throws SystemException
	 *             Caso ocorra alguma exceção ao executar o método.
	 */
	public static boolean isAnnotationPresenteNoCampo(Object o,
			String propriedade, Class<? extends Annotation> annotation)
	throws SystemException {
		boolean retorno = false;

		try {

			Class<?> clazz = o.getClass();
			if (campoExiste(clazz, propriedade)){
				Field field = clazz.getDeclaredField(propriedade);

				field.setAccessible(true);

				retorno = field.isAnnotationPresent(annotation);
			}
		} catch (Exception ex) {
			logger.error("Error: ",ex);
			throw new SystemException(ErrorCore.ERROR_SYSTEM, ex);
		}

		return retorno;
	}

	/**
	 * Verifica se a propriedade foi declarada na classe.
	 * 
	 * @param propriedade
	 *            a propriedade a ser declarada.
	 * @param clazz
	 *            a classe a ser verificada
	 * @return true caso a propriedade tenha sido declarada, false caso não
	 *         tenha sido.
	 * @throws SystemException
	 *             Caso ocorra alguma exceção ao executar o método.
	 */
	public static boolean campoExiste(Class<?> clazz, String propriedade)
			throws SystemException {

		try {
			for (Field field : clazz.getDeclaredFields()) {
				field.setAccessible(true);

				if (field.getName().equals(propriedade))
					return true;
			}
		} catch (Exception e) {
			logger.error(e.getMessage(), e);
			throw new SystemException(ErrorCore.ERROR_SYSTEM);
		}

		return false;
	}

	/**
	 * Verifica se a propriedade declarada na classe eh estatica.
	 * 
	 * @param o
	 *            um Object contendo o objeto
	 * @param propriedade
	 *            a propriedade a ser verificada.
	 * @return <code>true</code> caso a propriedade tenha seja estatica,
	 *         <code>false</code> caso não seja.
	 * @throws SystemException
	 *             Caso ocorra alguma exceção ao executar o método.
	 */
	public static boolean isCampoEstatico(Object o, String propriedade)
			throws SystemException {
		boolean retorno = false;

		try {
			Class<?> clazz = o.getClass();

			Field field = clazz.getDeclaredField(propriedade);
			field.setAccessible(true);

			retorno = Modifier.isStatic(field.getModifiers());
		} catch (Exception e) {
			logger.error(e.getMessage(), e);
			// throw new SystemException(CoreErroEnumaration.ERROR_SYSTEM);
		}

		return retorno;
	}

	/**
	 * Executa o método no objeto informado.
	 * 
	 * @param nomeMetodo
	 *            O nome do método que deve ser invocado.
	 * @param objeto
	 *            O objeto que sofrerá a ação do método.
	 * @return O objeto após o ação do método realizada.
	 * @throws SystemException
	 *             Caso ocorra alguma exceção ao executar o método.
	 */
	public static Object executarMetodoSemParametros(String nomeMetodo,
			Object objeto) throws SystemException {

		Class<?> classe = objeto.getClass();

		try {

			Method metodo = classe.getMethod(nomeMetodo, new Class[] {});
			objeto = metodo.invoke(objeto, new Object[] {});

		} catch (Exception e) {
			logger.error(e.getMessage(), e);
			throw new SystemException(ErrorCore.ERROR_SYSTEM,e);
		}

		return objeto;
	}

	/**
	 * Retorna uma String representando as propriedades do Bean e seus valores,
	 * somente serao impressas propriedades que são números ou String.
	 * 
	 * @param objeto
	 *            O objeto que sofrerá a ação do método.
	 * @return A String contendo os valores do bean.
	 */
	public static String obterPropriedades(Object objeto) {
		StringBuilder sb = new StringBuilder();

		try {
			String propChave = null;

			Map<String, Object> objMap = beanToMap(objeto);
			Set<String> propriedades = objMap.keySet();
			Iterator<String> it = propriedades.iterator();

			while (it.hasNext()) {

				propChave = it.next();
				Object o = objMap.get(propChave);

				if (o instanceof Number || o instanceof String) {
					sb.append(propChave + ": " + o);
					if (it.hasNext())
						sb.append(", ");
				}
			}

		} catch (Exception e) {
			logger.error(e.getMessage(), e);
		}

		return sb.toString();
	}
}
